#! python

# script for running the BLOG examples from the build directory and
# checking the result

import sys, os, subprocess

WILDCARD = '*'
APPROXIMATE = '~'

def compare_output(pattern, output):
  failure = 0
  
  if len(pattern) != len(output):
    failure = 1
    print "Pattern has %d lines while output has %d" % (len(pattern),
                                                        len(output))

  unmatched_output = 0

  patlnum = 0
  outlnum = 0

  while patlnum < len(pattern) and outlnum < len(output):
    patline = pattern[patlnum]
    outline = output[outlnum]

    if not wildcard_match(patline, outline):
      failure = 1

      if not unmatched_output:
        print "Unmatched output:"
        unmatched_output = 1
      
      print outline
      
    else:
      patlnum += 1
      
    outlnum += 1

  if outlnum < len(output):
    if not unmatched_output:
      print "Unmatched output:"
    for ol in output[outlnum:]:
      print ol

  if patlnum < len(pattern):
    print "Unmatched pattern:"
    for pl in pattern[patlnum:]:
      print pl

  if failure:
    sys.exit(1)

NUMERIC_CHARS = set(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".",
                     "e", "E", "+", "-"])

# find the position of the first non-numeric character or -1
def find_non_num(string, startpos=0):
  for pos in range(startpos, len(string)):
    if string[pos] not in NUMERIC_CHARS:
      return pos

  return -1
  
def wildcard_match(patline, outline):
  if outline == patline:
    return 1

  outpos = 0
  patpos = 0
  while patpos < len(patline):
    # a wildcard at the end of the line matches the rest of the output
    # it even matches if the output is exhausted
    if patline[patpos] == WILDCARD and patpos == len(patline)-1:
      return 1

    # if we have exhausted the output then the pattern is not being matched
    if outpos >= len(outline):
      return 0

    # if we have an approximate number then we should get both the numbers
    # and check for a match
    if patline[patpos] == APPROXIMATE:
      patpos += 1

      patpos_end = find_non_num(patline, patpos)
      if patpos_end == -1:
        pat_num = float(patline[patpos:])
      else:
        pat_num = float(patline[patpos:patpos_end])
      
      outpos_end = find_non_num(outline, outpos)
      if outpos_end == -1:
        out_num = float(outline[outpos:])
      else:
        out_num = float(outline[outpos:outpos_end])

      # if the two numbers differ by more than 20% then there is no match
      if (out_num != pat_num and
          abs((out_num - pat_num)/(out_num/2 + pat_num/2)) > .2):
        return 0
      
      if patpos_end == -1:
        patpos = len(patline)
      else:
        patpos = patpos_end
      
      if outpos_end == -1:
        outpos = len(outline)
      else:
        outpos = outpos_end
      
      continue

    # if the next character matches exactly, we need to move forward trivially
    if patline[patpos] == outline[outpos]:
      outpos += 1
      patpos += 1
      continue

    # if the next pattern char is a wildcard we need to skip over the input
    # till it matches the pattern char after the wildcard
    if patline[patpos] == WILDCARD:
      patpos += 1
      while outpos < len(outline) and patline[patpos] != outline[outpos]:
        outpos += 1

      if outpos < len(outline):
        patpos += 1
        outpos += 1
        continue
      else:
        return 0

    # otherwise, the pattern and the output char don't match, we have failed
    else:
      return 0

  # the pattern is exhaused, so the output better be exhausted as well
  if outpos < len(outline):
    return 0
  else:
    return 1

def run_cmd(cmd):
  p = subprocess.Popen(cmd, shell=True,
                       stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  output, err = p.communicate()
  if p.returncode != 0:
    print "FAILURE running the following command:"
    print cmd
    print output+err
    sys.exit(1)
    
  return output

def compile_blog(blog_files):
  # C compiler
  cc = os.getenv('cc')
  if cc is None:
    cc = 'cc'

  # BLOG compiler
  blogc = os.getenv('blogc')
  if blogc is None:
    blogc = 'blogc'

  # figure out the prefix of the generated file
  prefix = os.path.splitext(os.path.basename(blog_files[0]))[0]

  # compile the file
  run_cmd(blogc + " " + " ".join(blog_files))

  cfile = prefix + ".c"
  if not os.path.exists(cfile):
    sys.exit(1)

  # if necessary change the include directories
  libblogc_incdir = os.getenv('libblogc_incdir')
  if libblogc_incdir is not None:
    fin = open(cfile, "r")
    lines = fin.readlines()
    fin.close()

    INC_PREFIX = '#include <blogc/'
    fout = open(cfile, "w")
    for line in lines:
      if line.startswith(INC_PREFIX):
        # replace > with "
        closeidx = line.find('>')
        if closeidx < 0:
          sys.exit(1)
        line = list(line)
        line[closeidx] = '"'
        line = "".join(line)
        
        fout.write('#include "' + libblogc_incdir + '/' \
                   + line[len(INC_PREFIX):])
      else:
        fout.write(line)
    fout.close()
    
  libblogc_libdir = os.getenv('libblogc_libdir')
  if libblogc_libdir is not None:
    libdir = " -L " + libblogc_libdir
  else:
    libdir = ""

  # compile the file
  run_cmd(cc + " " + cfile + " -o " + prefix + libdir + " -lblogc -lm")
  
  #os.remove(cfile)
  return prefix

COMMAND = '/* TESTBLOG:'
COMMAND_END = '*/'

def run_blog(prefix, blog_files):
  num_cmds = 0
  for fname in blog_files:
    
    cmd_out = None
    cmd_pat = None
    
    for line in open(fname).readlines():
      if cmd_out is not None:
        if line.startswith(COMMAND_END):
          compare_output(cmd_pat, cmd_out)
          cmd_out = None
          cmd_pat = None
        else:
          cmd_pat.append(line.rstrip())
      
      elif line.startswith(COMMAND):
        num_cmds += 1
        args = line[len(COMMAND):]
        cmd_out = run_cmd("./" + prefix + " " + args).rstrip()\
                  .split(os.linesep)
        cmd_pat = []

  # we should have run at least one command
  if num_cmds == 0:
    sys.exit(1)

  #os.remove(prefix)
  
if __name__ == "__main__":
  prefix = compile_blog(sys.argv[1:])
  run_blog(prefix, sys.argv[1:])
  
  sys.exit(0)

